package polimi.reds.optimizer.algorithm;

import java.util.ArrayList;


import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import polimi.reds.Message;
import polimi.reds.NodeDescriptor;
import polimi.reds.broker.overlay.Overlay;
import polimi.reds.broker.routing.Router;
import polimi.reds.broker.routing.SubscriptionTable;
import polimi.reds.optimizer.AbstractOptimizerStrategy;
import polimi.reds.optimizer.TrafficLock;

public class OCBROptimizerStrategy extends AbstractOptimizerStrategy {

	// Traffic counters, each traffic key represents a filter and a node
	private Map<TrafficKey, Integer> trafficMap = new HashMap<TrafficKey, Integer>();
	
	private long updateEvents;
	private long eventsBeforeUpdate;
	private Random rnd;
	private String id;
	private int threshold;
	
	@Override
	public void initialize(Overlay overlay, Router router, Map<String,String> options, TrafficLock trafficLock) {
		super.initialize(overlay, router, options, trafficLock);
		rnd=new Random(System.currentTimeMillis()+overlay.getNodeDescriptor().hashCode());
		this.id = overlay.getNodeDescriptor().getID();
		this.updateEvents = Integer.parseInt(options.get(PARAM_UPDATEEVENTS));
		this.eventsBeforeUpdate = Integer.parseInt(options.get(PARAM_UPDATEEVENTS));
		this.threshold = Integer.parseInt(options.get(PARAM_THRESHOLD));
	}
	
	private void tryReconfiguration() {
		if (eventsBeforeUpdate>0) { // || getOverlay().getNumberOfBrokers()>maxNeighbors) {
			return;
		}
	
		List<NodeDescriptor> nodesToRewire = chooseNodesToRewire();
		
		if (nodesToRewire == null) {
			logger.fine(id + " has no broker neighbors to rewire. (from a total of " + getOverlay().getNumberOfBrokers() + " neighbors)");
			return;
		}
		
		//Double savedMessages = testNeighbors(nodesToRewire.get(0), nodesToRewire.get(1));
		tryToSwapNeighbor(nodesToRewire.get(0), getOverlay().getNodeDescriptor(),nodesToRewire.get(1));		
		//eventsBeforeUpdate = updateEvents;
		eventsBeforeUpdate = (long) (updateEvents*(50.0+rnd.nextDouble()*100.0)/100.0);
	}

	/**
	 * Chooses two suitable nodes to rewire.
	 * 
	 * @return a List that contains two nodes that shold be connected together (the first node should be disconnected from the local node)
	 */
	private List<NodeDescriptor> chooseNodesToRewire() {
		
		List<NodeDescriptor> rNeighbors = new ArrayList<NodeDescriptor>(getOverlay().getNeighbors());		
		Collections.shuffle(rNeighbors,rnd);
		
		//List<Node> rNeighbors = new ArrayList<Node>();
		
		//for (Integer i=0; i<20 && cNeighbors.size() > 0; i++) {
		//	rNeighbors.add(cNeighbors.remove(random.nextInt(cNeighbors.size())));
		//}
		
		
		List<NodeDescriptor> nodesToRewire = new ArrayList<NodeDescriptor>();
		
		NodeDescriptor n1 = null;
		NodeDescriptor n2 = null;
		Integer max = 0;
		Integer tmp = 0;
		
		for (Integer i=0; i<rNeighbors.size(); i++) {
			if (rNeighbors.get(i).isClient()) {
				continue;
			}
			//System.out.print("-");
			for (Integer j=0; j<rNeighbors.size(); j++) {
				if (i==j || rNeighbors.get(i).equals(this) || rNeighbors.get(j).equals(this) || rNeighbors.get(j).isClient()) {
					continue;
				}
				//System.out.print(".");
				
				tmp = evaluateReconfiguration(rNeighbors.get(i), rNeighbors.get(j));
				if (tmp > max) {
					max = tmp;
				    n1 = rNeighbors.get(i);
				    n2 = rNeighbors.get(j);
				}
			}
		}
		
		if ((n1 == null) || (n2 == null)) {
			return null;
		}
		
		nodesToRewire.add(n1);
		nodesToRewire.add(n2);
		
		logger.fine(id + ": rewiring -(" + getOverlay().getNodeDescriptor().getID() + 
				"," + n1.getID() + ") +(" + n1.getID() + "," + n2.getID() + ") will save " + max + 
				" messages (> threshold=" + (0-threshold) + ")");	
		return nodesToRewire;
	}

	private int evaluateReconfiguration(NodeDescriptor n1,
			NodeDescriptor n2) {
		
		TrafficKey key = new TrafficKey(n1, n2);
		
		Integer traffic = trafficMap.get(key);
		
		if (traffic == null) {
			traffic = 0;
		}
		
		logger.fine("Expected traffic improvement: " + traffic);
		
		return traffic;
	}

	@Override
	protected void brokersUpdateEvent(Collection<NodeDescriptor> nodes) {
		
		if (nodes.size() == 0) {
			return;
		}
		
		//Collection<TrafficKey> knownKeys = new ArrayList<TrafficKey>(trafficMap.keySet());
		//
		//for (TrafficKey k:knownKeys) {
		//	if (!nodes.contains(k.getNode1()) || !nodes.contains(k.getNode2()) ) {
		//		trafficMap.remove(k);
		//	}
		//}
		
		trafficMap.clear();
	}

	@Override
	protected void postRoutingTrafficEvent(NodeDescriptor source,
			NodeDescriptor destination, Message data) {

		TrafficKey key = new TrafficKey(source, destination);
		Integer traffic = trafficMap.get(key);
		
		if (traffic == null) {
			traffic = 0;
		}
		
		traffic++;
		
		trafficMap.put(key, traffic);
		
		eventsBeforeUpdate--;
		tryReconfiguration();
	}

	@Override
	protected void preRoutingTrafficEvent(NodeDescriptor source, Message data) {
	}

	@Override
	protected void subscriptionTableUpdateEvent(
			SubscriptionTable subscriptionTable) {
		tryReconfiguration();
	}
	
	private class TrafficKey {
		private NodeDescriptor n1;
		private NodeDescriptor n2;
		
		public TrafficKey(NodeDescriptor n1, NodeDescriptor n2) {
			this.n1 = n1;
			this.n2 = n2;
		}
		
		public boolean equals(Object obj) {
			if (!(obj instanceof TrafficKey)) {
				return super.equals(obj);
			}
			
			return (obj.hashCode() == hashCode());
		}
		
		public String toString() {
			if (n1.hashCode() > n2.hashCode()) {
				return n1 + " " + n2;
			} else {
				return n2 + " " + n1;
			}
		}
		
		public int hashCode() {
			return n1.toString().hashCode()/2 + n2.toString().hashCode()/2;
		}
		
		public NodeDescriptor getNode1() {
			return n1;
		}
		
		public NodeDescriptor getNode2() {
			return n2;
		}
	}

}